package eu.inloop.viewmodel; import android.app.Activity; import android.content.Intent; import android.databinding.DataBindingUtil; import android.databinding.ViewDataBinding; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import java.util.UUID; import eu.inloop.viewmodel.binding.ViewModelBindingConfig; public class ViewModelHelper<T extends IView, R extends AbstractViewModel<T>> { @NonNull private static final String STATE_STRING_SCREEN_IDENTIFIER = ViewModelHelper.class + ".state.string.identifier"; //NON-NLS @Nullable private String mScreenId; @Nullable private R mViewModel; @Nullable private ViewDataBinding mBinding; private boolean mModelRemoved; private boolean mOnSaveInstanceCalled; /** * Call from {@link android.app.Activity#onCreate(android.os.Bundle)} or * {@link android.support.v4.app.Fragment#onCreate(android.os.Bundle)} * * @param activity parent activity * @param savedInstanceState savedInstance state from {@link Activity#onCreate(Bundle)} or * {@link Fragment#onCreate(Bundle)} * @param viewModelClass the {@link Class} of your ViewModel * @param arguments pass {@link Fragment#getArguments()} or * {@link Activity#getIntent()}.{@link Intent#getExtras() getExtras()} */ public void onCreate(@NonNull Activity activity, @Nullable Bundle savedInstanceState, @Nullable Class<? extends AbstractViewModel<T>> viewModelClass, @Nullable Bundle arguments) { // no viewmodel for this fragment if (viewModelClass == null) { mViewModel = null; return; } // screen (activity/fragment) created for first time, attach unique ID if (savedInstanceState == null) { mScreenId = UUID.randomUUID().toString(); } else { mScreenId = savedInstanceState.getString(STATE_STRING_SCREEN_IDENTIFIER); if (null == mScreenId) { throw new IllegalStateException("Bundle from onSaveInstanceState() didn't contain screen identifier. " + //NON-NLS "Did you call ViewModelHelper.onSaveInstanceState?"); //NON-NLS } mOnSaveInstanceCalled = false; } // get model instance for this screen final ViewModelProvider viewModelProvider = getViewModelProvider(activity).getViewModelProvider(); if (null == viewModelProvider) { throw new IllegalStateException("ViewModelProvider for activity " + activity + " was null."); //NON-NLS } final ViewModelProvider.ViewModelWrapper<T> viewModelWrapper = viewModelProvider.getViewModel(mScreenId, viewModelClass); //noinspection unchecked mViewModel = (R) viewModelWrapper.viewModel; if (viewModelWrapper.wasCreated) { // detect that the system has killed the app - saved instance is not null, but the model was recreated if (BuildConfig.DEBUG && savedInstanceState != null) { Log.d("model", "Fragment recreated by system or ViewModelStatePagerAdapter - restoring viewmodel"); //NON-NLS } mViewModel.onCreate(arguments, savedInstanceState); } } /** * Call from {@link android.support.v4.app.Fragment#onViewCreated(android.view.View, android.os.Bundle)} * or {@link android.app.Activity#onCreate(android.os.Bundle)} * * @param view view */ public void setView(@NonNull final T view) { if (mViewModel == null) { //no viewmodel for this fragment return; } mViewModel.onBindView(view); } public void performBinding(@NonNull final IView bindingView) { // skip if already create if (mBinding != null) { return; } // get ViewModelBinding config final ViewModelBindingConfig viewModelConfig = bindingView.getViewModelBindingConfig(); // if fragment not providing ViewModelBindingConfig, do not perform binding operations if (viewModelConfig == null) { return; } // perform Data Binding initialization final ViewDataBinding viewDataBinding; if (bindingView instanceof Activity) { viewDataBinding = DataBindingUtil.setContentView(((Activity) bindingView), viewModelConfig.getLayoutResource()); } else if (bindingView instanceof Fragment) { viewDataBinding = DataBindingUtil.inflate(LayoutInflater.from(viewModelConfig.getContext()), viewModelConfig.getLayoutResource(), null, false); } else { throw new IllegalArgumentException("View must be an instance of Activity or Fragment (support-v4)."); } // bind all together if (!viewDataBinding.setVariable(viewModelConfig.getViewModelVariableName(), getViewModel())) { throw new IllegalArgumentException("Binding variable wasn't set successfully. Probably viewModelVariableName of your " + "ViewModelBindingConfig of " + bindingView.getClass().getSimpleName() + " doesn't match any variable in " + viewDataBinding.getClass().getSimpleName()); } mBinding = viewDataBinding; } /** * Use in case this model is associated with an {@link android.support.v4.app.Fragment} * Call from {@link android.support.v4.app.Fragment#onDestroyView()}. Use in case model is associated * with Fragment * * @param fragment fragment */ public void onDestroyView(@NonNull Fragment fragment) { if (mViewModel == null) { //no viewmodel for this fragment return; } mViewModel.clearView(); if (fragment.getActivity() != null && fragment.getActivity().isFinishing()) { removeViewModel(fragment.getActivity()); } mBinding = null; } /** * Use in case this model is associated with an {@link android.support.v4.app.Fragment} * Call from {@link android.support.v4.app.Fragment#onDestroy()} * * @param fragment fragment */ public void onDestroy(@NonNull final Fragment fragment) { if (mViewModel == null) { //no viewmodel for this fragment return; } if (fragment.getActivity().isFinishing()) { removeViewModel(fragment.getActivity()); } else if (fragment.isRemoving() && !mOnSaveInstanceCalled) { // The fragment can be still in backstack even if isRemoving() is true. // We check mOnSaveInstanceCalled - if this was not called then the fragment is totally removed. if (BuildConfig.DEBUG) { Log.d("mode", "Removing viewmodel - fragment replaced"); //NON-NLS } removeViewModel(fragment.getActivity()); } mBinding = null; } /** * Use in case this model is associated with an {@link android.app.Activity} * Call from {@link android.app.Activity#onDestroy()} * * @param activity activity */ public void onDestroy(@NonNull final Activity activity) { if (mViewModel == null) { //no viewmodel for this fragment return; } mViewModel.clearView(); if (activity.isFinishing()) { removeViewModel(activity); } mBinding = null; } /** * Call from {@link android.app.Activity#onStop()} or {@link android.support.v4.app.Fragment#onStop()} */ public void onStop() { if (mViewModel == null) { //no viewmodel for this fragment return; } mViewModel.onStop(); } /** * Call from {@link android.app.Activity#onStart()} ()} or {@link android.support.v4.app.Fragment#onStart()} ()} */ public void onStart() { if (mViewModel == null) { //no viewmodel for this fragment return; } mViewModel.onStart(); } /** * Returns the current ViewModel instance associated with the Fragment or Activity. * Throws an {@link IllegalStateException} in case the ViewModel is null. This can happen * if you call this method too soon - before {@link Activity#onCreate(Bundle)} or {@link Fragment#onCreate(Bundle)} * or this {@link ViewModelHelper} is not properly setup. * * @return {@link R} */ @NonNull public R getViewModel() { if (null == mViewModel) { throw new IllegalStateException("ViewModel is not ready. Are you calling this method before Activity/Fragment onCreate?"); //NON-NLS } return mViewModel; } /** * Call from {@link android.app.Activity#onSaveInstanceState(android.os.Bundle)} * or {@link android.support.v4.app.Fragment#onSaveInstanceState(android.os.Bundle)}. * This allows the model to save its state. * * @param bundle bundle */ public void onSaveInstanceState(@NonNull Bundle bundle) { bundle.putString(STATE_STRING_SCREEN_IDENTIFIER, mScreenId); if (mViewModel != null) { mViewModel.onSaveInstanceState(bundle); mOnSaveInstanceCalled = true; } } @Nullable public ViewDataBinding getBinding() { return mBinding; } public void removeViewModel(@NonNull final Activity activity) { if (mViewModel != null && !mModelRemoved) { final ViewModelProvider viewModelProvider = getViewModelProvider(activity).getViewModelProvider(); if (null == viewModelProvider) { throw new IllegalStateException("ViewModelProvider for activity " + activity + " was null."); //NON-NLS } viewModelProvider.remove(mScreenId); mViewModel.onDestroy(); mModelRemoved = true; mBinding = null; } } @NonNull private IViewModelProvider getViewModelProvider(@NonNull Activity activity) { if (!(activity instanceof IViewModelProvider)) { throw new IllegalStateException("Your activity must implement IViewModelProvider"); //NON-NLS } return ((IViewModelProvider) activity); } }